﻿// 2023/5/4: 首个版本
// 2023/5/5: 新增OnePythonTemplate.ToXml
// 2023/6/19: 修正Split问题
// 2023/10/9: 添加英文注释
// 2024/2/27: 通用样本通道最多支持到24路
// 2024/2/28: 信号输入绑定参数改为optional
// 2024/4/17: 支持API3接口
// 2024/6/25: 继承ModuleConfig。支持异步
// 2024/6/26: 输出语言代号zh
// 2024/6/28: 恢复方法名
// 2024/6/28: 新增OnePythonTemplate.FromXml
// 2024/6/28: PythonTemplates支持client模式
// 2025/2/28: 修复coreEnsureTemplateTable
// 2025/7/15: 新增OnePythonTemplate.RequiredPython3MinorVersion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.IO;
using ASEva;
using ASEva.Utility;
using System.Threading.Tasks;

namespace CommonSource
{
    struct SignalInputConfig
    {
        public String paramName;
        public String signalSource;
        public double defaultValue;
        public bool nearest;
    }

    struct SampleOutputConfig
    {
        public String aliasName;
        public GeneralSampleTitle title;
    }

    class ParameterConfig
    {
        public String Value { get; set; }
        public String Comment { get; set; }
    }

    class SampleProtocolComps
    {
        public String Basic { get; private set; }
        public int? Version { get; private set; } // 1~99
        public int Channel { get; private set; } // -1 indicates invalid / -1表示无效

        public override bool Equals(object obj)
        {
            var target = obj as SampleProtocolComps;
            if (Basic == target.Basic && Channel == target.Channel)
            {
                if (Version == null) return target.Version == null;
                else return target.Version != null && target.Version == Version.Value;
            }
            else return false;
        }

        public override int GetHashCode()
        {
            return ToString().GetHashCode();
        }

        public override string ToString()
        {
            var versionPostfix = Version == null ? "" : ("-v" + Version.Value);
            var channelPostfix = Channel >= 0 ? ("@" + Channel) : "";
            return Basic + versionPostfix + channelPostfix;
        }

        public static SampleProtocolComps FromProtocol(String protocol, Dictionary<String, ParameterConfig> parameters, bool strict = false)
        {
            var comps = new SampleProtocolComps();
            comps.Channel = -1;

            var atIndex = protocol.LastIndexOf('@');
            if (atIndex > 0)
            {
                var channelText = protocol.Substring(atIndex + 1);
                if (channelText.Length > 2 && channelText.StartsWith('{') && channelText.EndsWith('}'))
                {
                    var paramName = channelText.Substring(1, channelText.Length - 2);
                    if (parameters != null && parameters.ContainsKey(paramName)) channelText = parameters[paramName].Value;
                }

                int channel = -1;
                if (Int32.TryParse(channelText, out channel) && channel >= 0 && channel < 24)
                {
                    comps.Channel = channel;
                    protocol = protocol.Substring(0, atIndex);
                }
                else if (strict) return null;
            }

            var barVIndex = protocol.LastIndexOf("-v");
            if (barVIndex > 0)
            {
                var versionText = protocol.Substring(barVIndex + 2);

                int version = -1;
                if (Int32.TryParse(versionText, out version) && version >= 1 && version < 100)
                {
                    comps.Version = version;
                    protocol = protocol.Substring(0, barVIndex);
                }
                else if (strict) return null;
            }
            
            comps.Basic = protocol;
            return comps;
        }
    }

    class CommonScriptConfig
    {
        public int SamplingRate { get; set; }
        public List<String> Commands { get; set; }
        public String CommandStart { get; set; }
        public String CommandEnd { get; set; }
        public List<SignalInputConfig> SignalInputConfigs { get; set; }
        public List<String> SignalOutputNames { get; set; }
        public List<String> SampleInputs { get; set; }
        public Dictionary<String, SampleOutputConfig> SampleOutputs { get; set; }
        public Dictionary<String, SceneTitle> SceneOutputs { get; set; }
        public Dictionary<String, GraphDefinition> ReportOutputs { get; set; }
        public Dictionary<String, ParameterConfig> Parameters { get; set; }

        public CommonScriptConfig()
        {
            SamplingRate = 20;
            Commands = new List<String>();
            CommandStart = null;
            CommandEnd = null;
            SignalInputConfigs = new List<SignalInputConfig>();
            SignalOutputNames = new List<String>();
            SampleInputs = new List<String>();
            SampleOutputs = new Dictionary<String, SampleOutputConfig>();
            SceneOutputs = new Dictionary<String, SceneTitle>();
            ReportOutputs = new Dictionary<String, GraphDefinition>();
            Parameters = new Dictionary<String, ParameterConfig>();
        }

        public static CommonScriptConfig Init(XmlElement sNode)
        {
            var cp = new AttributeParser(sNode);
            var cc = new CommonScriptConfig();

            var samplingRate = Math.Max(5, Math.Min(100, cp.ParseInt("freq", 20)));
            cc.SamplingRate = samplingRate;

            foreach (XmlElement inNode in sNode.GetElementsByTagName("in"))
            {
                var scp = new AttributeParser(inNode);
                var nearest = scp.ParseBool("nearest", "yes", false);
                var signalSource = scp.ParseString("signal", null);
                var param = scp.ParseString("param", null);
                if (signalSource == null || signalSource.Length == 0 || param == null || param.Length == 0) continue;

                var config = new SignalInputConfig();
                config.paramName = param;
                config.signalSource = signalSource;
                config.defaultValue = scp.ParseDouble("default_val", 0);
                config.nearest = nearest;
                cc.SignalInputConfigs.Add(config);
            }

            foreach (XmlElement outNode in sNode.GetElementsByTagName("out"))
            {
                var scp = new AttributeParser(outNode);
                var param = scp.ParseString("param", null);
                if (param == null || param.Length == 0) continue;

                cc.SignalOutputNames.Add(param);
            }

            foreach (XmlElement sampleInNode in sNode.GetElementsByTagName("sample_in"))
            {
                cc.SampleInputs.Add(sampleInNode.InnerText);
            }

            foreach (XmlElement sampleOutNode in sNode.GetElementsByTagName("sample_out"))
            {
                var scp = new AttributeParser(sampleOutNode);
                var oid = scp.ParseString("id", null);
                var oname = scp.ParseString("name", null);
                var otitle = sampleOutNode.InnerText;
                if (oid != null && oname != null && oid.Length > 0 && oname.Length > 0)
                {
                    cc.SampleOutputs[oid] = new SampleOutputConfig()
                    {
                        aliasName = oname,
                        title = new GeneralSampleTitle() { Titles = otitle.Length == 0 ? new List<String>() : otitle.Split(',').ToList() }
                    };
                }
            }

            foreach (XmlElement sceneOutNode in sNode.GetElementsByTagName("scene_out"))
            {
                var scp = new AttributeParser(sceneOutNode);
                var baseSceneID = scp.ParseString("id", null);
                if (baseSceneID == null || baseSceneID.Length == 0) continue;
                var titles = new SceneTitle();
                titles.Titles = sceneOutNode.InnerText.Length == 0 ? new List<String>() : sceneOutNode.InnerText.Split(',').ToList();
                cc.SceneOutputs[baseSceneID] = titles;
            }

            foreach (XmlElement reportOutNode in sNode.GetElementsByTagName("report_out"))
            {
                var scp = new AttributeParser(reportOutNode);
                var id = scp.ParseString("id", null);
                var type = (GraphType)scp.ParseEnum("type", typeof(GraphType), GraphType.Invalid);
                var title = scp.ParseString("title", null);
                if (type == GraphType.Invalid || id == null || id.Length == 0 || title == null || title.Length == 0) continue;

                var columnTitlesText = scp.ParseString("column_titles", "");
                var configsText = scp.ParseString("configs", "");

                var columnTitles = String.IsNullOrEmpty(columnTitlesText) ? new String[0] : columnTitlesText.Split(',');
                var configs = String.IsNullOrEmpty(configsText) ? new String[0] : configsText.Split(',');
                var def = new GraphDefinition();
                def.Type = type;
                def.MainTitle = title;
                def.Config = configs.ToList();
                def.ColumnTitles = columnTitles.ToList();
                cc.ReportOutputs[id] = def;
            }

            foreach (XmlElement paramNode in sNode.GetElementsByTagName("param"))
            {
                var scp = new AttributeParser(paramNode);
                var name = scp.ParseString("name", null);
                var value = scp.ParseString("value", null);
                var comment = paramNode.InnerText;
                if (name == null || value == null || name.Length == 0) continue;
                cc.Parameters[name] = new ParameterConfig()
                {
                    Value = value,
                    Comment = comment == null ? "" : comment,
                };
            }

            foreach (XmlElement cmdNode in sNode.GetElementsByTagName("cmd"))
            {
                var bytes = Convert.FromBase64String(cmdNode.InnerText);
                if (bytes == null || bytes.Length == 0) continue;
                cc.Commands.AddRange(PythonScriptUtil.GenCommands(Encoding.UTF8.GetString(bytes)));
            }

            var cmdStartNodes = sNode.GetElementsByTagName("cmd_start");
            var cmdEndNodes = sNode.GetElementsByTagName("cmd_end");
            if (cmdStartNodes != null && cmdStartNodes.Count > 0)
            {
                var bytes = Convert.FromBase64String(cmdStartNodes[0].InnerText);
                if (bytes != null && bytes.Length != 0) cc.CommandStart = Encoding.UTF8.GetString(bytes);
            }
            if (cmdEndNodes != null && cmdEndNodes.Count > 0)
            {
                var bytes = Convert.FromBase64String(cmdEndNodes[0].InnerText);
                if (bytes != null && bytes.Length != 0) cc.CommandEnd = Encoding.UTF8.GetString(bytes);
            }

            return cc;
        }

        public void GetConfig(XmlElement sNode)
        {
            var xml = sNode.OwnerDocument;
            var cw = new AttributeWriter(sNode);

            cw.WriteInt("freq", SamplingRate);

            for (int i = 0; i < SignalInputConfigs.Count; i++)
            {
                var config = SignalInputConfigs[i];

                var scw = new AttributeWriter(sNode.AppendChild(xml.CreateElement("in")) as XmlElement);
                scw.WriteString("signal", config.signalSource);
                scw.WriteBool("nearest", config.nearest, "yes", "no");
                scw.WriteString("param", config.paramName);
                scw.WriteDouble("default_val", config.defaultValue);
            }

            foreach (var name in SignalOutputNames)
            {
                var scw = new AttributeWriter(sNode.AppendChild(xml.CreateElement("out")) as XmlElement);
                scw.WriteString("param", name);
            }

            foreach (var protocol in SampleInputs)
            {
                sNode.AppendChild(xml.CreateElement("sample_in")).InnerText = protocol;
            }

            foreach (var pair in SampleOutputs)
            {
                var sampleOutNode = sNode.AppendChild(xml.CreateElement("sample_out")) as XmlElement;
                var scw = new AttributeWriter(sampleOutNode);
                scw.WriteString("id", pair.Key);
                scw.WriteString("name", pair.Value.aliasName);
                sampleOutNode.InnerText = String.Join(",", pair.Value.title.Titles);
            }

            foreach (var pair in SceneOutputs)
            {
                var sceneOutNode = sNode.AppendChild(xml.CreateElement("scene_out")) as XmlElement;
                var scw = new AttributeWriter(sceneOutNode);
                scw.WriteString("id", pair.Key);
                sceneOutNode.InnerText = String.Join(",", pair.Value.Titles);
            }

            foreach (var pair in ReportOutputs)
            {
                var reportOutNode = sNode.AppendChild(xml.CreateElement("report_out")) as XmlElement;
                var scw = new AttributeWriter(reportOutNode);
                scw.WriteString("id", pair.Key);
                scw.WriteString("type", pair.Value.Type.ToString());
                scw.WriteString("title", pair.Value.MainTitle);
                scw.WriteString("configs", String.Join(",", pair.Value.Config));
                scw.WriteString("column_titles", String.Join(",", pair.Value.ColumnTitles));
            }

            foreach (var param in Parameters)
            {
                if (param.Key.Length == 0) continue;
                var paramNode = sNode.AppendChild(xml.CreateElement("param")) as XmlElement;
                var scw = new AttributeWriter(paramNode);
                scw.WriteString("name", param.Key);
                scw.WriteString("value", param.Value.Value);
                paramNode.InnerText = param.Value.Comment;
            }

            foreach (var cmd in Commands)
            {
                sNode.AppendChild(xml.CreateElement("cmd")).InnerText = Convert.ToBase64String(Encoding.UTF8.GetBytes(cmd));
            }

            if (CommandStart != null) sNode.AppendChild(xml.CreateElement("cmd_start")).InnerText = Convert.ToBase64String(Encoding.UTF8.GetBytes(CommandStart));
            if (CommandEnd != null) sNode.AppendChild(xml.CreateElement("cmd_end")).InnerText = Convert.ToBase64String(Encoding.UTF8.GetBytes(CommandEnd));
        }
    }

    class NormalModeScriptConfig
    {
        public String NormalID { get; set; }
        public bool TypeAutoConversion { get; set; }
        public bool IDLegacyMode { get; set; }
        public CommonScriptConfig Common { get; set; }

        public NormalModeScriptConfig()
        {
            NormalID = null;
            TypeAutoConversion = true; // Enable for new config / 新配置默认开
            IDLegacyMode = false; // Disable for new config / 新配置默认关
            Common = new CommonScriptConfig();
        }
    }

    class TemplateModeScriptConfig
    {
        public String TemplatePostfix { get; set; }
        public int? OverwriteSamplingRate { get; set; }
        public Dictionary<String, String> OverwriteParameters { get; set; }

        public TemplateModeScriptConfig()
        {
            TemplatePostfix = null;
            OverwriteSamplingRate = null;
            OverwriteParameters = new Dictionary<String, String>();
        }
    }

    class OnePythonScriptConfig
    {
        public bool Enabled { get; set; }
        public String NameEnglish { get; set; }
        public String NameChinese { get; set; }
        public String TemplateID { get; set; } // Empty indicates normal mode, otherwise template mode / 空表示一般模式，有值表示模板模式
        public object ModeConfig { get; set; } // NormalModeScriptConfig for normal mode, otherwise TemplateModeScriptConfig / 一般模式为NormalModeScriptConfig，模板模式为TemplateModeScriptConfig

        public static OnePythonScriptConfig Init(XmlElement sNode)
        {
            var cp = new AttributeParser(sNode);

            var enabled = cp.ParseBool("enabled", "yes", false);
            var nameEnglish = cp.ParseString("name", null);
            var nameChinese = cp.ParseString("name_zh", null);
            if (nameChinese == null) nameChinese = cp.ParseString("name_ch", null);
            var templateID = cp.ParseString("template", null);
            if (String.IsNullOrEmpty(nameEnglish)) return null;

            var config = new OnePythonScriptConfig();
            config.Enabled = enabled;
            config.NameEnglish = nameEnglish;
            config.NameChinese = String.IsNullOrEmpty(nameChinese) ? nameEnglish : nameChinese;
            config.TemplateID = templateID;

            if (String.IsNullOrEmpty(templateID))
            {
                var normalID = cp.ParseString("id", null);
                if (String.IsNullOrEmpty(normalID)) return null;

                var sc = new NormalModeScriptConfig();
                sc.NormalID = normalID;
                sc.TypeAutoConversion = cp.ParseBool("type_auto_conversion", "yes", false); // Disable for old config / 老配置默认关
                sc.IDLegacyMode = cp.ParseBool("id_legacy_mode", "yes", true); // Enable for old config / 老配置默认开
                sc.Common = CommonScriptConfig.Init(sNode);

                config.ModeConfig = sc;
            }
            else
            {
                var templatePostfix = cp.ParseString("template_postfix", null);
                if (String.IsNullOrEmpty(templatePostfix)) return null;

                var tc = new TemplateModeScriptConfig();
                tc.TemplatePostfix = templatePostfix;

                var overwriteFreq = cp.ParseInt("overwrite_freq", 0);
                if (overwriteFreq != 0) tc.OverwriteSamplingRate = Math.Max(5, Math.Min(100, overwriteFreq));

                foreach (XmlElement paramNode in sNode.GetElementsByTagName("overwrite_param"))
                {
                    var scp = new AttributeParser(paramNode);
                    var name = scp.ParseString("name", null);
                    var value = scp.ParseString("value", null);
                    if (name == null || value == null || name.Length == 0) continue;
                    tc.OverwriteParameters[name] = value;
                }

                config.ModeConfig = tc;
            }

            return config;
        }

        public void GetConfig(XmlElement sNode)
        {
            var xml = sNode.OwnerDocument;
            var cw = new AttributeWriter(sNode);

            cw.WriteBool("enabled", Enabled, "yes", "no");
            cw.WriteString("name", NameEnglish);
            cw.WriteString("name_zh", NameChinese);
            cw.WriteString("template", TemplateID == null ? "" : TemplateID);

            if (String.IsNullOrEmpty(TemplateID))
            {
                var sc = ModeConfig as NormalModeScriptConfig;
                cw.WriteString("id", sc.NormalID);
                cw.WriteBool("type_auto_conversion", sc.TypeAutoConversion, "yes", "no");
                cw.WriteBool("id_legacy_mode", sc.IDLegacyMode, "yes", "no");
                sc.Common.GetConfig(sNode);
            }
            else
            {
                var tc = ModeConfig as TemplateModeScriptConfig;
                cw.WriteString("template_postfix", tc.TemplatePostfix);
                if (tc.OverwriteSamplingRate != null) cw.WriteInt("overwrite_freq", tc.OverwriteSamplingRate.Value);

                foreach (var param in tc.OverwriteParameters)
                {
                    if (param.Key.Length == 0) continue;
                    var paramNode = sNode.AppendChild(xml.CreateElement("overwrite_param")) as XmlElement;
                    var scw = new AttributeWriter(paramNode);
                    scw.WriteString("name", param.Key);
                    scw.WriteString("value", param.Value);
                }
            }
        }

        public String ScriptID
        {
            get
            {
                if (String.IsNullOrEmpty(TemplateID)) return (ModeConfig as NormalModeScriptConfig).NormalID;
                else return TemplateID + "-" + (ModeConfig as TemplateModeScriptConfig).TemplatePostfix;
            }
        }

        public String ScriptName
        {
            get
            {
                var chinese = AgencyLocal.GetAppLanguage() == Language.Chinese;
                var name = chinese ? NameChinese : NameEnglish;
                if (String.IsNullOrEmpty(name)) name = chinese ? NameEnglish : NameChinese;
                if (String.IsNullOrEmpty(name)) name = "Unknown Script";
                return name;
            }
        }

        public CommonScriptConfig CommonConfig // null indicates invalid / null表示无效
        {
            get
            {
                if (String.IsNullOrEmpty(TemplateID)) return (ModeConfig as NormalModeScriptConfig).Common;
                else
                {
                    var template = PythonTemplates.GetTemplate(TemplateID);
                    return template == null ? null : template.CommonConfig;
                }
            }
        }

        public String SignalPackProtocol
        {
            get
            {
                String midID;
                if (String.IsNullOrEmpty(TemplateID))
                {
                    if ((ModeConfig as NormalModeScriptConfig).IDLegacyMode) midID = NameEnglish.ToLower().Replace(' ', '-');
                    else midID = (ModeConfig as NormalModeScriptConfig).NormalID;
                }
                else midID = TemplateID + "-" + (ModeConfig as TemplateModeScriptConfig).TemplatePostfix;
                return String.Format("script-{0}-signals", midID);
            }
        }

        public String SignalTypeAndGraphFix
        {
            get
            {
                if (String.IsNullOrEmpty(TemplateID))
                {
                    if ((ModeConfig as NormalModeScriptConfig).IDLegacyMode) return NameEnglish;
                    else return (ModeConfig as NormalModeScriptConfig).NormalID;
                }
                else return TemplateID + "-" + (ModeConfig as TemplateModeScriptConfig).TemplatePostfix;
            }
        }

        public String GetFinalGraphTitle(String graphFix, String mainTitle)
        {
            if (String.IsNullOrEmpty(TemplateID) && (ModeConfig as NormalModeScriptConfig).IDLegacyMode) return String.Format("{0} - {1}", graphFix, mainTitle);
            else return String.Format("{0} ({1})", mainTitle, graphFix);
        }

        public int FinalSamplingRate // 0 indicates invalid / 0表示无效
        {
            get
            {
                if (String.IsNullOrEmpty(TemplateID)) return (ModeConfig as NormalModeScriptConfig).Common.SamplingRate;
                else
                {
                    var template = PythonTemplates.GetTemplate(TemplateID);
                    if (template == null) return 0;
                    var overwriteSamplingRate = (ModeConfig as TemplateModeScriptConfig).OverwriteSamplingRate;
                    return template.CanOverwriteSamplingRate && overwriteSamplingRate != null ? overwriteSamplingRate.Value : template.CommonConfig.SamplingRate;
                }
            }
        }

        public Dictionary<String, ParameterConfig> FinalParameters // null indicates invalid / null表示无效
        {
            get
            {
                if (String.IsNullOrEmpty(TemplateID)) return (ModeConfig as NormalModeScriptConfig).Common.Parameters;
                else
                {
                    var template = PythonTemplates.GetTemplate(TemplateID);
                    if (template == null) return null;
                    var output = new Dictionary<String, ParameterConfig>();
                    foreach (var pair in template.CommonConfig.Parameters)
                    {
                        output[pair.Key] = new ParameterConfig
                        {
                            Value = pair.Value.Value,
                            Comment = pair.Value.Comment,
                        };
                    }
                    foreach (var pair in (ModeConfig as TemplateModeScriptConfig).OverwriteParameters)
                    {
                        if (output.ContainsKey(pair.Key)) output[pair.Key].Value = pair.Value;
                    }
                    return output;
                }
            }
        }

        public bool ShouldAutoConvertParameterType
        {
            get
            {
                if (String.IsNullOrEmpty(TemplateID)) return (ModeConfig as NormalModeScriptConfig).TypeAutoConversion;
                else return true;
            }
        }

        public String GetFinalSceneID(String baseSceneID)
        {
            if (String.IsNullOrEmpty(TemplateID)) return baseSceneID;
            else return String.Format("{0}-{1}", baseSceneID, (ModeConfig as TemplateModeScriptConfig).TemplatePostfix);
        }
    }

    class PythonScriptsConfig : ModuleConfig
    {
        public List<OnePythonScriptConfig> Scripts { get; set; }

        public PythonScriptsConfig()
        {
            Scripts = new List<OnePythonScriptConfig>();
        }

        public static async Task<PythonScriptsConfig> Load(object caller)
        {
            var config = new PythonScriptsConfig();
            var configString = await AgencyAsync.GetModuleConfig(caller, "python-scripts.pluginpython");
            config.SetConfig(configString);
            return config;
        }

        public Task Save(object caller)
        {
            return AgencyAsync.SetModuleConfig(caller, "python-scripts.pluginpython", GetConfig());
        }

        public override void DisableAll()
        {
            foreach (var s in Scripts) s.Enabled = false;
        }

        public override async Task DisableErrorPart()
        {
            foreach (var s in Scripts)
            {
                if (!(await PythonScriptUtil.IsScriptConfigValid(s.CommonConfig, s.FinalParameters)).Item1) s.Enabled = false;
            }
        }

        public override string GetConfig()
        {
            var xml = new XmlDocument();
            xml.AppendChild(xml.CreateXmlDeclaration("1.0", "utf-8", null));
            var root = xml.AppendChild(xml.CreateElement("root"));

            foreach (var s in Scripts)
            {
                var sNode = root.AppendChild(xml.CreateElement("s")) as XmlElement;
                s.GetConfig(sNode);
            }

            return xml.InnerXml;
        }

        public override void SetConfig(string config)
        {
            Scripts = new List<OnePythonScriptConfig>();

            XmlDocument xml = null;
            try
            {
                xml = new XmlDocument();
                xml.LoadXml(config);
            }
            catch (Exception) { return; }

            foreach (XmlElement sNode in xml.DocumentElement.GetElementsByTagName("s"))
            {
                var sc = OnePythonScriptConfig.Init(sNode);
                if (sc != null) Scripts.Add(sc);
            }
        }

        public override async Task<(ConfigStatus, String)> GetConfigStatus()
        {
            String error = null;

            var packProtocolFlags = new Dictionary<String, bool>();
            var graphFixFlags = new Dictionary<String, bool>();
            foreach (var s in Scripts)
            {
                if (!s.Enabled) continue;

                var packProtocol = s.SignalPackProtocol;
                var graphFix = s.SignalTypeAndGraphFix;
                if (packProtocolFlags.ContainsKey(packProtocol) || graphFixFlags.ContainsKey(graphFix)) 
                {
                    error = "Repeated ID";
                    return (ConfigStatus.EnabledWithError, error);
                }

                packProtocolFlags[packProtocol] = true;
                graphFixFlags[graphFix] = true;
            }

            int enableCount = 0, okCount = 0;
            foreach (var s in Scripts)
            {
                if (!s.Enabled) continue;
                enableCount++;

                var cc = s.CommonConfig;
                var result = await PythonScriptUtil.IsScriptConfigValid(cc, s.FinalParameters);
                error = result.Item2;
                if (cc == null || !result.Item1) continue;
                okCount++;
            }

            if (enableCount == 0) return (ConfigStatus.Disabled, error);
            else if (!AgencyLocal.ClientSide && !PythonScriptUtil.ImportPathsContainBiCommon())
            {
                error = "Can't find bi_common.py";
                return (ConfigStatus.EnabledWithError, error);
            }
            else if (enableCount == okCount) return (ConfigStatus.Enabled, error);
            else return (ConfigStatus.EnabledWithWarning, error);
        }

        public override List<SignalPackConfig> GetProcessorRelatedSignalPackings()
        {
            var list = new List<SignalPackConfig>();
            for (int i = 0; i < Scripts.Count; i++)
            {
                var s = Scripts[i];
                if (!s.Enabled) continue;

                var cc = s.CommonConfig;
                if (cc == null || cc.SignalInputConfigs.Count == 0) continue;

                var finalSamplingRate = s.FinalSamplingRate;
                if (finalSamplingRate == 0) continue;

                var finalParameters = s.FinalParameters;
                if (finalParameters == null) continue;

                var packConfigs = new SignalPackConfigElem[cc.SignalInputConfigs.Count];
                for (int n = 0; n < cc.SignalInputConfigs.Count; n++)
                {
                    var config = cc.SignalInputConfigs[n];
                    var signalID = PythonScriptUtil.GetSignalIDFromSource(config.signalSource, finalParameters);
                    if (signalID == null) signalID = "";

                    var packConfig = new SignalPackConfigElem
                    {
                        ValueID = signalID,
                        Scale = 1,
                        Timeout = 1200,
                        IsNearestMode = config.nearest,
                    };
                    packConfigs[n] = packConfig;
                }

                var pack = new SignalPackConfig();
                pack.Protocol = s.SignalPackProtocol;
                pack.IsInterpolationMode = true;
                pack.SamplingRate = finalSamplingRate;
                pack.SignalConfigs = packConfigs.ToList();
                list.Add(pack);
            }
            return list;
        }

        public override Dictionary<string, string[]> GetProcessorOutputSignalNameTable()
        {
            var table = new Dictionary<String, String[]>();
            foreach (var s in Scripts)
            {
                if (!s.Enabled) continue;

                var cc = s.CommonConfig;
                if (cc == null || cc.SignalOutputNames.Count == 0) continue;

                table[s.SignalTypeAndGraphFix] = cc.SignalOutputNames.ToArray();
            }
            return table;
        }

        public override List<ChannelAlias> GetChannelAliasList()
        {
            var list = new List<ChannelAlias>();
            foreach (var s in Scripts)
            {
                if (!s.Enabled) continue;

                var cc = s.CommonConfig;
                if (cc == null) continue;

                var parameters = s.FinalParameters;
                if (parameters == null) continue;

                foreach (var pair in cc.SampleOutputs)
                {
                    var comps = SampleProtocolComps.FromProtocol(pair.Key, parameters);
                    list.Add(new ChannelAlias()
                        {
                            ChannelID = comps.ToString(),
                            AliasName = pair.Value.aliasName,
                        });
                }
            }
            return list;
        }

        public override Dictionary<string, GeneralSampleTitle> GetProcessorOutputSampleTitles()
        {
            var table = new Dictionary<string, GeneralSampleTitle>();
            foreach (var s in Scripts)
            {
                if (!s.Enabled) continue;

                var cc = s.CommonConfig;
                if (cc == null) continue;

                var parameters = s.FinalParameters;
                if (parameters == null) continue;

                foreach (var pair in cc.SampleOutputs)
                {
                    var comps = SampleProtocolComps.FromProtocol(pair.Key, parameters);
                    table[comps.ToString()] = pair.Value.title;
                }
            }
            return table;
        }

        public override Dictionary<string, SceneTitle> GetProcessorOutputSceneTitles()
        {
            var table = new Dictionary<String, SceneTitle>();
            foreach (var s in Scripts)
            {
                if (!s.Enabled) continue;

                var cc = s.CommonConfig;
                if (cc == null) continue;

                foreach (var pair in cc.SceneOutputs)
                {
                    table[s.GetFinalSceneID(pair.Key)] = pair.Value;
                }
            }
            return table;
        }

        public override GraphDefinition[] GetProcessorOutputGraphDefinitions()
        {
            var table = new Dictionary<int, GraphDefinition>();
            foreach (var s in Scripts)
            {
                if (!s.Enabled) continue;

                var cc = s.CommonConfig;
                if (cc == null) continue;

                var graphFix = s.SignalTypeAndGraphFix;
                foreach (var pair in cc.ReportOutputs)
                {
                    var defWithScriptName = new GraphDefinition();
                    defWithScriptName.MainTitle = s.GetFinalGraphTitle(graphFix, pair.Value.MainTitle);
                    defWithScriptName.Type = pair.Value.Type;
                    defWithScriptName.Config = pair.Value.Config;
                    defWithScriptName.ColumnTitles = pair.Value.ColumnTitles;
                    defWithScriptName.Validation = pair.Value.Validation;
                    table[defWithScriptName.GetID()] = defWithScriptName;
                }
            }
            return table.Values.ToArray();
        }
    }

    class OnePythonTemplate : IComparable
    {
        public String TemplateID { get; set; }
        public String TitleEnglish { get; set; }
        public String TitleChinese { get; set; }
        public String Version { get; set; }
        public String DependAPI { get; set; }
        public String DeprecatedAPI { get; set; }
        public List<String> UsedAPIs { get; set; }
        public int? RequiredPython3MinorVersion { get; set; }
        public String Signature { get; set; }
        public CommonScriptConfig CommonConfig { get; set; }
        public bool CanOverwriteSamplingRate { get; set; }

        public static OnePythonTemplate Create(String templateID, List<OnePythonTemplate> otherConfigs)
        {
            if (String.IsNullOrWhiteSpace(templateID)) return null;

            int i = 0;
            String nameEn = null, nameZh = null;
            while (true)
            {
                var indexText = ++i > 1 ? i.ToString() : "";
                var targetEnglishName = "Unnamed Template" + (indexText.Length > 0 ? (" " + indexText) : "");
                var targetChineseName = "未命名模板" + indexText;
                if (otherConfigs.Exists(c => c.TitleEnglish == targetEnglishName) || 
                    otherConfigs.Exists(c => c.TitleChinese == targetChineseName)) continue;
                nameEn = targetEnglishName;
                nameZh = targetChineseName;
                break;
            }

            var template = new OnePythonTemplate();
            template.TemplateID = templateID;
            template.TitleEnglish = nameEn;
            template.TitleChinese = nameZh;
            template.Version = "1.0.0";
            template.DependAPI = "";
            template.DeprecatedAPI = "";
            template.UsedAPIs = new List<string>();
            template.Signature = "";
            template.CommonConfig = new CommonScriptConfig();
            template.CanOverwriteSamplingRate = false;
            return template;
        }

        public static OnePythonTemplate Load(String xmlFilePath, String templateID, List<OnePythonTemplate> otherConfigs)
        {
            var xml = new XmlDocument();
            try { xml.Load(xmlFilePath); }
            catch (Exception) { return null; }

            var cp = new AttributeParser(xml.DocumentElement);
            var rootNode = xml.DocumentElement;
            if (rootNode.Name != "python_template") return null;

            var ver = cp.ParseString("version", "");
            if (String.IsNullOrEmpty(ver)) ver = "1.0.0";

            var nameEn = cp.ParseString("name", null);
            var nameZh = cp.ParseString("name_zh", null);
            if (nameZh == null) nameZh = cp.ParseString("name_ch", null);
            if (nameEn != null && nameEn.Length > 0)
            {
                if (otherConfigs.Exists(c => c.TitleEnglish == nameEn)) nameEn = null;
            }
            if (nameZh != null && nameZh.Length > 0)
            {
                if (otherConfigs.Exists(c => c.TitleChinese == nameZh)) nameZh = null;
            }
            if (nameEn == null || nameEn.Length == 0)
            {
                int i = 0;
                while (true)
                {
                    var targetName = "Unnamed Template" + (++i > 1 ? (" " + i) : "");
                    if (otherConfigs.Exists(c => c.TitleEnglish == targetName)) continue;
                    nameEn = targetName;
                    break;
                }
            }
            if (nameZh == null || nameZh.Length == 0)
            {
                int i = 0;
                while (true)
                {
                    var targetName = "未命名模板" + (++i > 1 ? i.ToString() : "");
                    if (otherConfigs.Exists(c => c.TitleChinese == targetName)) continue;
                    nameZh = targetName;
                    break;
                }
            }

            var dependAPI = cp.ParseString("depend_api", null);
            if (dependAPI == null) dependAPI = "";

            var deprecatedAPI = cp.ParseString("deprecated_api", null);
            if (deprecatedAPI == null) deprecatedAPI = "";

            var requiredPython3MinorVersion = cp.ParseInt("required_python3_minor_version", -1);

            var signature = cp.ParseString("signature", null);
            if (signature == null) signature = "";

            var usedAPIs = new List<String>();
            foreach (XmlElement useApiNode in rootNode.GetElementsByTagName("use_api"))
            {
                usedAPIs.Add(useApiNode.InnerText);
            }

            var common = CommonScriptConfig.Init(rootNode);
            var canOverwriteSamplingRate = cp.ParseBool("can_overwrite_freq", "yes", false);

            var template = new OnePythonTemplate();
            template.TemplateID = templateID;
            template.TitleEnglish = nameEn;
            template.TitleChinese = nameZh;
            template.Version = ver;
            template.DependAPI = dependAPI;
            template.DeprecatedAPI = deprecatedAPI;
            template.UsedAPIs = usedAPIs;
            template.RequiredPython3MinorVersion = requiredPython3MinorVersion >= 0 ? requiredPython3MinorVersion : null;
            template.Signature = signature;
            template.CommonConfig = common;
            template.CanOverwriteSamplingRate = canOverwriteSamplingRate;
            return template;
        }

        public static OnePythonTemplate LoadInLibrary(String templateID, List<OnePythonTemplate> otherConfigs)
        {
            if (AgencyLocal.ClientSide) return null;

            var xmlFilePath = PythonScriptUtil.GetTemplateRoot() + Path.DirectorySeparatorChar + templateID + ".xml";
            if (!File.Exists(xmlFilePath)) return null;
            
            return Load(xmlFilePath, templateID, otherConfigs);
        }

        public static OnePythonTemplate FromXml(String templateID, XmlDocument xml)
        {
            if (String.IsNullOrEmpty(templateID) || xml == null) return null;

            var cp = new AttributeParser(xml.DocumentElement);
            var template = new OnePythonTemplate();
            template.TemplateID = templateID;
            template.TitleEnglish = cp.ParseString("name", "Unnamed Template");
            template.TitleChinese = cp.ParseString("name_zh", "未命名模板");
            template.Version = cp.ParseString("version", "1.0.0");
            template.DependAPI = cp.ParseString("depend_api", "");
            template.DeprecatedAPI = cp.ParseString("deprecated_api", "");
            template.RequiredPython3MinorVersion = cp.ParseInt("required_python3_minor_version", -1);
            if (template.RequiredPython3MinorVersion < 0) template.RequiredPython3MinorVersion = null;
            template.Signature = cp.ParseString("signature", "");
            template.CanOverwriteSamplingRate = cp.ParseBool("can_overwrite_freq", "yes", false);
            template.UsedAPIs = new List<string>();
            foreach (XmlElement useApiNode in xml.DocumentElement.GetElementsByTagName("use_api"))
            {
                template.UsedAPIs.Add(useApiNode.InnerText);
            }
            template.CommonConfig = CommonScriptConfig.Init(xml.DocumentElement);
            return template;
        }

        public XmlDocument ToXml()
        {
            var xml = Xml.Create("python_template");
            var cw = new AttributeWriter(xml.DocumentElement);
            cw.WriteString("name", TitleEnglish);
            cw.WriteString("name_zh", TitleChinese);
            cw.WriteString("version", Version);
            cw.WriteString("depend_api", DependAPI);
            cw.WriteString("deprecated_api", DeprecatedAPI);
            cw.WriteInt("required_python3_minor_version", RequiredPython3MinorVersion ?? -1);
            cw.WriteString("signature", Signature);
            cw.WriteBool("can_overwrite_freq", CanOverwriteSamplingRate, "yes", "no");

            foreach (var api in UsedAPIs)
            {
                xml.DocumentElement.AppendChild(xml.CreateElement("use_api")).InnerText = api;
            }

            CommonConfig.GetConfig(xml.DocumentElement);

            return xml;
        }

        public void Save()
        {
            if (AgencyLocal.ClientSide) return;

            var xml = ToXml();
            var xmlFilePath = PythonScriptUtil.GetTemplateRoot() + Path.DirectorySeparatorChar + TemplateID + ".xml";
            xml.Save(xmlFilePath);
        }

        public int CompareTo(object obj)
        {
            var lang = AgencyLocal.GetAppLanguage();
            bool chinese = false;
            if (lang == Language.Invalid) chinese = DefaultCompareChinese;
            else chinese = lang == Language.Chinese;
            if (chinese) return TitleChinese.CompareTo((obj as OnePythonTemplate).TitleChinese);
            else return TitleEnglish.CompareTo((obj as OnePythonTemplate).TitleEnglish);
        }

        public static bool DefaultCompareChinese { get; set; }
    }

    class PythonTemplates
    {
        public static String[] GetTemplateIDs()
        {
            if (!AgencyLocal.ClientSide) coreEnsureTemplateTable();

            lock (templateTable)
            {
                return templateTable.Keys.ToArray();
            }
        }

        public static bool ContainsTemplate(String templateID)
        {
            if (String.IsNullOrEmpty(templateID)) return false;

            if (!AgencyLocal.ClientSide) coreEnsureTemplateTable();

            lock (templateTable)
            {
                return templateTable.ContainsKey(templateID);
            }
        }

        public static OnePythonTemplate GetTemplate(String templateID)
        {
            if (String.IsNullOrEmpty(templateID)) return null;

            if (!AgencyLocal.ClientSide) coreEnsureTemplateTable();

            lock (templateTable)
            {
                if (templateTable.ContainsKey(templateID)) return templateTable[templateID];
                if (AgencyLocal.ClientSide) return null;
                var template = OnePythonTemplate.LoadInLibrary(templateID, templateTable.Values.ToList());
                if (template != null)
                {
                    if (Validator == null || Validator.ValidateTemplate(template)) templateTable[templateID] = template;
                }
                return template;
            }
        }

        public static OnePythonTemplate[] GetAllTemplates()
        {
            if (!AgencyLocal.ClientSide) coreEnsureTemplateTable();

            lock (templateTable)
            {
                return templateTable.Values.ToArray();
            }
        }

        public static void CoreRefresh()
        {
            var list = new List<OnePythonTemplate>();
            foreach (var file in Directory.GetFiles(PythonScriptUtil.GetTemplateRoot(), "*.xml"))
            {
                var templateID = Path.GetFileNameWithoutExtension(file);
                var template = OnePythonTemplate.Load(file, templateID, list);
                if (template != null)
                {
                    if (Validator == null || Validator.ValidateTemplate(template)) list.Add(template);
                }
            }
            list.Sort();

            var newTable = new Dictionary<String, OnePythonTemplate>();
            foreach (var template in list)
            {
                newTable[template.TemplateID] = template;
            }

            lock (templateTable)
            {
                templateTable = newTable;
            }

            coreUpdateGlobalVariables();
        }

        public static async Task ClientRefresh()
        {
            if (interval.Test())
            {
                var ids = (await AgencyAsync.GetGlobalVariable("Python.TemplateList", "")).Split('\n');

                var tasks = new List<Task<String>>();
                foreach (var id in ids) tasks.Add(AgencyAsync.GetGlobalVariable("Python.Template." + id, ""));
                var results = await Task.WhenAll(tasks);

                var newTemplates = new List<OnePythonTemplate>();
                for (int i = 0; i < ids.Length; i++)
                {
                    var id = ids[i];
                    var xmlString = results[i];
                    if (String.IsNullOrWhiteSpace(xmlString)) continue;

                    var xml = new XmlDocument();
                    try { xml.LoadXml(xmlString); }
                    catch (Exception) { continue; }

                    var template = OnePythonTemplate.FromXml(id, xml);
                    if (template != null) newTemplates.Add(template);
                }

                lock (templateTable)
                {
                    templateTable.Clear();
                    foreach (var template in newTemplates) templateTable[template.TemplateID] = template;
                }
            }
        }

        public static bool Remove(String templateID, bool delete)
        {
            if (String.IsNullOrEmpty(templateID)) return false;
            if (AgencyLocal.ClientSide) return false;

            coreEnsureTemplateTable();

            if (delete)
            {
                var targetXmlPath = PythonScriptUtil.GetTemplateRoot() + Path.DirectorySeparatorChar + templateID + ".xml";
                if (File.Exists(targetXmlPath))
                {
                    if (!AgencyLocal.DeleteToRecycleBin(targetXmlPath))
                    {
                        try { File.Delete(targetXmlPath); }
                        catch (Exception) { return false; }
                    }
                }
            }

            bool removed = false;
            lock (templateTable)
            {
                if (templateTable.ContainsKey(templateID))
                {
                    templateTable.Remove(templateID);
                    removed = true;
                }
            }

            coreUpdateGlobalVariables();

            return removed;
        }

        public interface TemplateValidator
        {
            bool ValidateTemplate(OnePythonTemplate template);
        }

        public static TemplateValidator Validator { private get; set; }

        private static void coreEnsureTemplateTable()
        {
            if (!coreRefreshed)
            {
                CoreRefresh();
                coreRefreshed = true;
            }
        }

        private static void coreUpdateGlobalVariables()
        {
            lock (templateTable)
            {
                var ids = templateTable.Keys.ToArray();
                AgencyLocal.SetGlobalVariable("Python.TemplateList", String.Join('\n', ids));
                foreach (var id in ids)
                {
                    var templateXml = templateTable[id].ToXml().InnerXml;
                    if (templateXml != null) AgencyLocal.SetGlobalVariable("Python.Template." + id, templateXml);
                }
            }
        }

        private static Dictionary<String, OnePythonTemplate> templateTable = new Dictionary<String, OnePythonTemplate>();
        private static bool coreRefreshed = false;
        private static IntervalControl interval = new IntervalControl(0.8);
    }

    class PythonScriptUtil
    {
        public static String GetSignalIDFromSource(String signalSource, Dictionary<String, ParameterConfig> parameters)
        {
            if (String.IsNullOrEmpty(signalSource) || parameters == null) return null;

            String signalID = signalSource;
            if (signalSource.Length > 2 && signalSource.StartsWith('{') && signalSource.EndsWith('}'))
            {
                var paramName = signalSource.Substring(1, signalSource.Length - 2);
                if (!parameters.ContainsKey(paramName) || String.IsNullOrEmpty(parameters[paramName].Value)) return null;
                signalID = parameters[paramName].Value;
            }

            return signalID;
        }

        public static async Task<(bool, String)> IsScriptConfigValid(CommonScriptConfig cc, Dictionary<String, ParameterConfig> parameters)
        {
            String error = null;
            if (cc == null)
            {
                error = "Null config";
                return (false, error);
            }
            foreach (var config in cc.SignalInputConfigs)
            {
                if (config.paramName == null || config.paramName.Length == 0)
                {
                    error = "Empty parameter name of input signal";
                    return (false, error);
                }
                var signalID = PythonScriptUtil.GetSignalIDFromSource(config.signalSource, parameters);
                if (!await AgencyAsync.IsSignalValid(signalID, true))
                {
                    error = "Invalid signal ID: " + signalID;
                    return (false, error);
                }
            }
            foreach (var name in cc.SignalOutputNames)
            {
                if (name == null || name.Length == 0)
                {
                    error = "Empty parameter name of output signal";
                    return (false, error);
                }
            }
            foreach (var elem in cc.SampleInputs)
            {
                var comps = SampleProtocolComps.FromProtocol(elem, parameters, true);
                if (comps == null)
                {
                    error = "Invalid input sample protocol: " + elem;
                    return (false, error);
                }
            }
            foreach (var pair in cc.SampleOutputs)
            {
                var comps = SampleProtocolComps.FromProtocol(pair.Key, parameters, true);
                if (comps == null)
                {
                    error = "Invalid output sample protocol: " + pair.Key;
                    return (false, error);
                }
            }
            return (true, error);
        }

        public static List<String> GenCommands(String script)
        {
            var list = new List<String>();

            String buffer = "";
            foreach (var rowText in script.Split('\n'))
            {
                if (rowText.Length == 0) continue;

                bool content = false;
                foreach (var c in rowText.ToCharArray())
                {
                    if ((c >= 'A' && c <= 'Z') ||
                        (c >= 'a' && c <= 'z') ||
                        (c >= '0' && c <= '9'))
                    {
                        content = true;
                        break;
                    }
                }
                if (!content) continue;

                bool pending = rowText[0] == ' ' || rowText[0] == '\t' ||
                    rowText.StartsWith("else") || rowText.StartsWith("elif") ||
                    rowText.StartsWith("except") || rowText.StartsWith("finally");
                if (pending)
                {
                    if (buffer.Length == 0) buffer += rowText;
                    else buffer += "\n" + rowText;
                }
                else
                {
                    if (buffer.Length > 0) list.Add(buffer);
                    buffer = rowText;
                }
            }

            if (buffer.Length > 0) list.Add(buffer);

            return list;
        }

        public static bool ImportPathsContainBiCommon()
        {
            if (AgencyLocal.ClientSide) return false;

            var importPathsText = AgencyLocal.GetGlobalPath("PythonImportPaths");
            if (importPathsText == null || importPathsText.Length == 0) return false;

            foreach (var path in importPathsText.Split(';'))
            {
                if (!Directory.Exists(path)) continue;
                foreach (var file in Directory.GetFiles(path))
                {
                    if (Path.GetFileName(file).ToLower() == "bi_common.py") return true;
                }
            }
            return false;
        }

        public static String GetTemplateRoot()
        {
            if (AgencyLocal.ClientSide) return null;

            var root = AgencyLocal.GetAppFilesRoot() + Path.DirectorySeparatorChar + "python";
            if (!Directory.Exists(root)) Directory.CreateDirectory(root);
            return root;
        }
    }
}
